/* 
 * File:    Client for "Tiny World of Warcraft" (TWW)
 * Authors: Jonathan Kotker (ee122-bw) and Shaan Mulchandani (ee122-bv)
 */

#define MIN(a, b) ((a) > (b) ? (b) : (a))
#define MAX(a, b) ((a) < (b) ? (b) : (a))

#include "messages.h"
#include "fileio.h"
#include "packetAssembler.h"
#include "packetContents.h"
#include "packetParser.h"
#include "playerList.h"
#include "playerStatistics.h"
#include "playerListNode.h"
#include "stringUtils.h"

#include <arpa/inet.h>
#include <ctype.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

/*
 * Sends the data in the `bytes' array. This method keeps sending until
 * all of the data has been sent, as the `send' method, by itself, does
 * not send all of the data in one go.
 */
void sendBytes(int socketFd, char *bytes, int len) {
    int bytesSent = 0, total = 0, bytesLeft = len;

    while (total < len) {
        bytesSent = send(socketFd, bytes + total, bytesLeft, 0);
        if (bytesSent == -1) {
            // Connection to server is broken.
            close(socketFd);
            on_disconnection_from_server();
            exit(EXIT_FAILURE);
        }
        total += bytesSent;
        bytesLeft -= bytesSent;
    }
};

/*
 * Returns a 1 if the `other' player is visible to you, based on
 * your current position; 0 otherwise.
 */
int withinRange(playerStatistics* self, playerStatistics* other) {
    if (self == NULL || other == NULL) return NULL;

    int playerXLoc = other->xLoc;
    int playerYLoc = other->yLoc;
    int currXLoc = self->xLoc;
    int currYLoc = self->yLoc;

    int currXMin = MAX(0, currXLoc - 5);
    int currXMax = MIN(99, currXLoc + 5);
    int currYMin = MAX(0, currYLoc - 5);
    int currYMax = MIN(99, currYLoc + 5);

    return (currXMin <= playerXLoc) && (playerXLoc <= currXMax) &&
            (currYMin <= playerYLoc) && (playerYLoc <= currYMax);
};

/*
 * Processes a packet sent by the server, by looking at the
 * message type contained in `contentsReceived' and appropriately
 * updating the state of the relevant client: the self is `self',
 * and the player list is stored in `playerList'.
 */
void processServerPacket(packetContents* contentsReceived,
        playerStatistics* self, playerList* currentPlayers, int* loggedIn) {

    // Grab the necessary statistics from `contentsReceived'.
    playerStatistics statsRecvd;
    statsRecvd.hp = contentsReceived->hp;
    statsRecvd.exp = contentsReceived->exp;
    statsRecvd.xLoc = contentsReceived->xLoc;
    statsRecvd.yLoc = contentsReceived->yLoc;

    strncpy(statsRecvd.name, contentsReceived->victimName,
            EE122_MAX_NAME_LENGTH);
    statsRecvd.name[EE122_MAX_NAME_LENGTH] = 0;

    switch (contentsReceived->messageType) {
        case LOGIN_REPLY:
            /*
             * Set your own player statistics. LOGIN_REPLY only cares
             * about our own client, whereas MOVE_NOTIFY tells us who
             * to update by searching for the name.
             */
            if ((!(*loggedIn)) && contentsReceived->errorCode == 0) {
                updateStats(self, &statsRecvd);
                *loggedIn = 1;
            }
            on_login_reply(contentsReceived->errorCode);
            break;
        case MOVE_NOTIFY:
            /*
             * The algorithm is as follows:
             * Compare the victimName to self->name to see if it is
             *  the client itself.
             * Else, scan the linked list of players (currentPlayers)
             *  to see if we have a match.
             * If we have a match, then update the details of that
             *  node/player.
             * If we do not get a match, then add a new player to
             *  the list with the stored details.
             */

            // Is the moving player yourself?
            if (strcmp(contentsReceived->victimName, self->name) == 0) {
                updateStats(self, &statsRecvd);
                on_move_notify(contentsReceived->victimName, self->xLoc,
                        self->yLoc, self->hp, self->exp);
            } else {
                addPlayer(currentPlayers, &statsRecvd);
                if (withinRange(self, &statsRecvd)) {
                    on_move_notify(statsRecvd.name, statsRecvd.xLoc,
                            statsRecvd.yLoc, statsRecvd.hp,
                            statsRecvd.exp);
                }
            }
            break;
        case ATTACK_NOTIFY: {
            playerListNode* attackerNode;
            playerListNode* victimNode;

            attackerNode = accessPlayer(currentPlayers,
                    contentsReceived->attackerName);

            victimNode = accessPlayer(currentPlayers,
                    contentsReceived->victimName);

            playerStatistics* attackerStats = (attackerNode == NULL) ?
                self : attackerNode->statistics;
            playerStatistics* victimStats = (victimNode == NULL) ?
                self : victimNode->statistics;

            if ((withinRange(self, attackerStats) &&
                    withinRange(self, victimStats))) {
                on_attack_notify(contentsReceived->attackerName,
                        contentsReceived->victimName,
                        contentsReceived->damage, contentsReceived->hp);
            }
            break;
        }
        case SPEAK_NOTIFY:
            on_speak_notify(contentsReceived->victimName,
                    contentsReceived->message);
            break;
        case LOGOUT_NOTIFY:
            removePlayer(currentPlayers, contentsReceived->victimName);
            on_exit_notify(contentsReceived->victimName);
            break;
        case INVALID_STATE:
            on_invalid_state(contentsReceived->errorCode);
            break;
    }
};

/*
 * Process a command supplied by the user.
 */
void processUserCommand(int socketFd,
        char* userInput, packetContents* contentsToSend,
        playerStatistics* self, playerList* currentPlayers, int* loggedIn) {

    char userCommand[MAX_COMMAND_LEN + 1];

    // LOGIN command?
    strncpy(userCommand, userInput, 5);
    userCommand[5] = 0x00;
    toLower(userCommand);

    if (strcmp(userCommand, "login") == 0) {
        contentsToSend->messageType = LOGIN_REQUEST;
        char *victimName = contentsToSend->victimName;

        strncpy(victimName, userInput + 6, EE122_MAX_NAME_LENGTH);

        replaceNewline(victimName);
        // End name with a null terminator.
        victimName[EE122_MAX_NAME_LENGTH] = 0;

        // Update the name in self, only if not already logged in.
        if (!(*loggedIn)) {
            strncpy(self->name, victimName, EE122_MAX_NAME_LENGTH);
            self->name[EE122_MAX_NAME_LENGTH] = 0;
        }

        char bytesToSend[16];
        int numBytes = assemblePacket(bytesToSend, contentsToSend);
        sendBytes(socketFd, bytesToSend, numBytes);
        return;
    }

    // SPEAK command?
    if (strcmp(userCommand, "speak") == 0) {
        contentsToSend->messageType = SPEAK;

        char *message = contentsToSend->message;

        strncpy(message, userInput + 6, EE122_MAX_MSG_LENGTH);

        replaceNewline(message);
        // End name with a null terminator.
        message[EE122_MAX_MSG_LENGTH] = 0;

        char bytesToSend[MAX_SEND_LEN];
        int numBytes = assemblePacket(bytesToSend, contentsToSend);
        sendBytes(socketFd, bytesToSend, numBytes);
        return;
    }

    // MOVE command?
    strncpy(userCommand, userInput, 4);
    userCommand[4] = 0x00;
    toLower(userCommand);

    if (strcmp(userCommand, "move") == 0) {
        contentsToSend->messageType = MOVE;

        char direction = tolower(userInput[5]);
        if (direction == 'n')
            contentsToSend->moveDir = NORTH;
        else if (direction == 's')
            contentsToSend->moveDir = SOUTH;
        else if (direction == 'w')
            contentsToSend->moveDir = WEST;
        else if (direction == 'e')
            contentsToSend->moveDir = EAST;

        char bytesToSend[8];
        int numBytes = assemblePacket(bytesToSend, contentsToSend);
        sendBytes(socketFd, bytesToSend, numBytes);
        return;
    }

    // ATTACK command?
    strncpy(userCommand, userInput, 6);
    userCommand[6] = 0x00;
    toLower(userCommand);

    if (strcmp(userCommand, "attack") == 0) {
        
        char opponentName[EE122_MAX_NAME_LENGTH + 1];
        strncpy(opponentName, userInput + 7, EE122_MAX_NAME_LENGTH);
        opponentName[EE122_MAX_NAME_LENGTH] = 0;
        replaceNewline(opponentName);

        playerListNode* opponentNode = accessPlayer(currentPlayers,
                opponentName);
        if (opponentNode == NULL) {
            on_not_visible();
            return;
        }
        playerStatistics* opponentStats = opponentNode->statistics;

        if ((strcmp(self->name, opponentName) == 0)) {
            fprintf(stdout, "No suicide allowed.");
            return;
        } else if (withinRange(self, opponentStats)) {
            strncpy(contentsToSend->victimName,
                    opponentName, EE122_MAX_NAME_LENGTH);
            contentsToSend->victimName[EE122_MAX_NAME_LENGTH] = 0;

            contentsToSend->messageType = ATTACK;

            char bytesToSend[12];
            int numBytes = assemblePacket(bytesToSend, contentsToSend);
            sendBytes(socketFd, bytesToSend, numBytes);
            show_prompt();
            return;
        } else {
            on_not_visible();
            return;
        }
    }

    // LOGOUT command?
    strncpy(userCommand, userInput, 6);
    userCommand[6] = 0x00;
    toLower(userCommand);

    if (strcmp(userCommand, "logout") == 0) {
        contentsToSend->messageType = LOGOUT;

        char bytesToSend[4];
        int numBytes = assemblePacket(bytesToSend, contentsToSend);
        sendBytes(socketFd, bytesToSend, numBytes);
        return;
    }
};

/*
 * Tasks to be run before exiting the program. `playerList' is the list
 * of current players, and `status' is the reason the program is exiting.
 */
void done(playerList* currentPlayers, int status) {
    clearList(currentPlayers);
    exit(status);
};

int main(int argc, char** argv) {

    // Set up variables to be used throughout the execution of the program.
    playerList currentPlayers;
    currentPlayers.head = NULL;
    playerStatistics self; // Our own data, for efficiency.
    int loggedIn = 0; // Whether we are logged in.

    if (argc != 5) {
        // Not enough arguments.
        fprintf(stdout, "Not enough arguments available. \n");
        done(&currentPlayers, EXIT_FAILURE);
    }

    char *serverIp;
    int serverPort;

    // Parse command-line arguments.
    int i = 1;
    for (i = 1; i < argc; i++) {
        if (strcmp(argv[i], "-s") == 0) {
            serverIp = argv[i + 1];
            i++;
        } else if (strcmp(argv[i], "-p") == 0) {
            serverPort = atoi(argv[i + 1]);
            i++;
        }
    }

    // Get socket file descriptor.
    int socketFd = socket(AF_INET, SOCK_STREAM, 0);
    if (socketFd < 0) {
        // Unable to create connection.
        on_client_connect_failure();
        done(&currentPlayers, EXIT_FAILURE);
    }

    // Connect to server.
    struct sockaddr_in sin;
    memset(&sin, 0, sizeof (sin));
    sin.sin_family = AF_INET; // IPv4.
    sin.sin_addr.s_addr = inet_addr(serverIp); // Server address.
    sin.sin_port = htons(serverPort); // Port on server.

    if (connect(socketFd, (struct sockaddr *) & sin, sizeof (sin)) < 0) {
        // Unable to create connection.
        close(socketFd);
        on_client_connect_failure();
        done(&currentPlayers, EXIT_FAILURE);
    };

    // Set up variables for infinite loop.
    fd_set readFds;

    // Struct to be converted into a packet to be sent to the server.
    packetContents contentsToSend;
    // Struct holding data contained in packet received from the server.
    packetContents contentsReceived;

    show_prompt();

    // Infinite loop of reading from the server or standard input.
    while (1) {
        // Zero out the read file descriptor set.
        FD_ZERO(&readFds);

        // Fill in the read file descriptor set.
        FD_SET(STDIN, &readFds);
        FD_SET(socketFd, &readFds);

        int result = select(socketFd + 1, &readFds, NULL, NULL, NULL);
        if (result == -1) {
            if (loggedIn == 0) {
                // Connection has failed.
                close(socketFd);
                on_disconnection_from_server();
                done(&currentPlayers, EXIT_FAILURE);
            } else {
                // User logged out.
                close(socketFd);
                done(&currentPlayers, EXIT_SUCCESS);
            }
        }

        if (result > 0) {
            // One of the file descriptors has data. Which one?            
            if (FD_ISSET(socketFd, &readFds)) {
                // Socket to the server has data. Deal with it.
                char packetBuffer[MAX_RECV_LEN];
                int result = recv(socketFd, packetBuffer, 3, 0);
                if (result <= 0) {
                    // Connection has failed.
                    close(socketFd);
                    on_disconnection_from_server();
                    done(&currentPlayers, EXIT_FAILURE);
                }

                // Retrieve length of packet.
                int length;
                length = (packetBuffer[1] << 8) | packetBuffer[2];

                result = recv(socketFd, packetBuffer + 3, length - 3, 0);
                if (result < length - 3) {
                    // Connection has failed.
                    close(socketFd);
                    on_malformed_message_from_server();
                    done(&currentPlayers, EXIT_FAILURE);
                }

                parsePacket(packetBuffer, &contentsReceived);
                processServerPacket(&contentsReceived, &self,
                        &currentPlayers, &loggedIn);
            }

            if (FD_ISSET(STDIN, &readFds)) {
                // User has typed in information. Deal with it.
                char userInputBuffer[MAX_INPUT_LEN + 1];
                fgets(userInputBuffer, MAX_INPUT_LEN + 1, stdin);

                processUserCommand(socketFd, userInputBuffer,
                        &contentsToSend, &self, &currentPlayers, &loggedIn);
            }
        }
    }

    close(socketFd);
    done(&currentPlayers, EXIT_SUCCESS);
};
